package cn.yeziji.forum.utils;

import cn.yeziji.forum.common.CaffeineZsetMap;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 缓存工具类
 *
 * <p>封装一级缓存(caffeine cache)和二级缓存(redis)的常用应用
 *
 * @author gzkemays
 * @since 2022/1/6 22:07
 */
@Component
public class CacheUtils {
  private static final String EXPIRED_PREFIX_KEY = "_CAFFEINE__EXPIRED_";

  private static final String CAFFEINE_ZSET_KEY = "_CAFFEINE__ZSET_";
  @Resource @Getter RedisUtils redisUtils;
  @Resource HttpServletResponse response;
  @Getter CaffeineUtils<Object> caffeine;
  @Getter Cache<String, Object> caffeineCache;
  @Getter RedisTemplate<String, Object> redisTemplate;
  @Getter long differenceTime;
  boolean customerCaffeine = false;
  boolean openCaffeineCache = false;

  @PostConstruct
  public void init() {
    this.redisTemplate = redisUtils.getRedisTemplate();
//    defaultCaffeineCache();
  }

  public CacheUtils() {}

  public CacheUtils(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
    this.redisUtils = new RedisUtils(this.redisTemplate);
  }

  public CacheUtils defaultCaffeineCache() {
    // 默认
    this.caffeineCache = Caffeine.newBuilder().initialCapacity(100).maximumSize(1024).build();
    this.caffeine = new CaffeineUtils<>(this.caffeineCache);
    this.openCaffeineCache = true;
    return this;
  }

  public CacheUtils newCaffeineCache(Cache cache) {
    this.caffeineCache = cache;
    this.customerCaffeine = true;
    this.openCaffeineCache = true;
    this.caffeine = new CaffeineUtils<>(caffeineCache);
    return this;
  }

  public CacheUtils closeCaffeineCache() {
    this.openCaffeineCache = false;
    return this;
  }

  public CacheUtils caffeineAddTime(long expired, TimeUnit timeUnit) {
    this.differenceTime = TimeUtils.getTime((int) expired, timeUnit);
    return this;
  }

  public void setResponseHeader(String header, String value) {
    response.setHeader(header, value);
  }

  /**
   * 保存数据至缓存
   *
   * <p>set 值无论 key 是否存在，都会对缓存进行一次刷新
   *
   * @param key 键值
   * @param value 值
   */
  public void set(String key, Object value, long time, TimeUnit timeUnit) {
    if (openCaffeineCache) {
      caffeine.putIfNoneRefresh(key, value);
      caffeineSetKeyExpired(key, time, timeUnit);
    }
    redisTemplate.opsForValue().set(key, value);
    redisTemplate.expire(key, time, timeUnit);
  }

  /**
   * hash 缓存
   *
   * @param key 哈希目录键值
   * @param hashKey 键值
   * @param value 数据
   * @param time 过期时间
   * @param timeUnit 过期单位
   */
  public void setHash(String key, String hashKey, Object value, long time, TimeUnit timeUnit) {
    if (openCaffeineCache) {
      caffeine.putHash(key, hashKey, value);
      caffeineSetKeyExpired(key, time, timeUnit);
    }
    redisTemplate.opsForHash().put(key, hashKey, value);
    redisTemplate.expire(key, time, timeUnit);
  }

  /**
   * 移除 hash 缓存
   *
   * @param key 目录键值
   * @param hashKey 哈希键值
   */
  public void removeHash(String key, String hashKey) {
    if (openCaffeineCache) {
      caffeine.remove(getCaffeineHashKey(key, hashKey));
    }
    if (Objects.nonNull(redisTemplate.opsForHash().get(key, hashKey))) {
      redisTemplate.opsForHash().delete(key, hashKey);
    }
  }

  /**
   * 模糊删除 hash 缓存
   *
   * <p>如果 blurKey 为空，则删除所有相关 key 的缓存。
   *
   * @param key 根键值
   * @param blurKey 模糊 hash 键值
   */
  public void blurRemoveHash(String key, String blurKey) {
    boolean blur = false;
    if (openCaffeineCache) {
      ConcurrentMap<String, Object> soc = caffeineCache.asMap();
      for (Map.Entry<String, Object> soe : soc.entrySet()) {
        String soeKey = soe.getKey();
        if (StringUtils.hasText(blurKey)) {
          // 如果 caffeine cache 的数据为 hashmap
          if (soeKey.contains(key) && soe.getValue() instanceof Map) {
            List<String> blurKeys = new ArrayList<>();
            Map<String, Object> tmp = (Map<String, Object>) soe.getValue();
            for (Map.Entry<String, Object> entry : tmp.entrySet()) {
              if (entry.getKey().contains(blurKey)) {
                // 统计 key 值
                blurKeys.add(entry.getKey());
              }
            }
            // 删除后重新赋值
            if (!blurKey.isEmpty()) {
              for (String bk : blurKeys) {
                tmp.remove(bk);
              }
              soe.setValue(tmp);
            }
          } else {
            blur = soeKey.contains(key) && soeKey.contains(blurKey);
          }
        } else {
          blur = soeKey.contains(key);
        }
        if (blur) {
          caffeineCache.invalidate(soeKey);
        }
      }
    }

    Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
    for (Map.Entry<Object, Object> entry : entries.entrySet()) {
      String entryKey = (String) entry.getKey();
      if (StringUtils.hasText(blurKey)) {
        blur = entryKey.contains(blurKey);
      }
      if (blur) {
        redisTemplate.opsForHash().delete(key, entryKey);
      } else {
        redisTemplate.delete(key);
      }
    }
  }

  /**
   * 根据 HashKey 获取键值下的所有值
   *
   * @param hashKey hashKey
   * @return 所有值
   */
  public List<Object> getHashValues(String hashKey) {
    List<Object> values = redisTemplate.opsForHash().values(hashKey);
    if (values.isEmpty()) {
      Object o = get(hashKey);
      if (o instanceof HashMap) {
        HashMap<Object, Object> map = (HashMap<Object, Object>) o;
        return Arrays.asList(map.values().toArray());
      }
    }
    return values;
  }

  public Map<Object, Object> getHashMaps(String hashKey) {
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(hashKey);
    if (entries.isEmpty()) {
      Object o = get(hashKey);
      if (o instanceof HashMap) {
        entries = (HashMap<Object, Object>) o;
      }
    }
    return entries;
  }

  public List<Map<Object, Object>> getBlurHashMap(String key) {
    return blurHashKeyToList(key, null, true).stream()
        .filter(obj -> obj instanceof Map)
        .map(obj -> (Map<Object, Object>) obj)
        .collect(Collectors.toList());
  }

  public List<Map<Object, Object>> getBlurHashMap(String key, String blurKey) {
    return blurHashKeyToList(key, blurKey, true).stream()
        .filter(obj -> obj instanceof Map)
        .map(obj -> (Map<Object, Object>) obj)
        .collect(Collectors.toList());
  }

  /**
   * 根据 key 来获取有关的 hash 数据
   *
   * @param key 键值
   * @return hash list
   */
  public List<Object> getBlurHashKey(String key) {
    return blurHashKeyToList(key, null, false);
  }

  /**
   * 具备关键字的模糊获取，该方法注重于从 redis 中捕获数据。
   *
   * @param key 前缀键值
   * @param blurKey 筛选词
   * @return list
   */
  public List<Object> getBlurHashKey(String key, String blurKey) {
    return blurHashKeyToList(key, blurKey, false);
  }

  /**
   * 模糊获取 Hash 键值的数据
   *
   * <ul>
   *   <li>如果存在筛选词则采用 {@link RedisUtils#blurHasKey(String)} 的方式从 redis 中捕获数据，如果不存在则仅进行简单的 {@link
   *       RedisUtils#hasKey(String)} 对 redis 进行检索。
   *   <li>因为如果存在筛选词，针对数据缓存能够更加有效的进行获取，否则数据量过多会导致效率低下。此时应该采取更加高效的方式，即针对从一级缓存中截取数据。
   * </ul>
   *
   * @param key 前缀键值
   * @param blurKey 筛选词
   * @return 捕获数据
   */
  private List<Object> blurHashKeyToList(String key, String blurKey, boolean toMap) {
    List<Object> list = new ArrayList<>();
    if (blurKey != null) {
      Set<String> keys = redisUtils.blurHasKey(key);
      if (keys != null && !CollectionUtils.isEmpty(keys)) {
        Set<String> collect =
            keys.stream().filter(k -> k.contains(blurKey)).collect(Collectors.toSet());
        for (String redisKey : collect) {
          Map<Object, Object> entries = redisTemplate.opsForHash().entries(redisKey);
          if (toMap) {
            list.add(entries);
          } else {
            for (Map.Entry<Object, Object> entry : entries.entrySet()) {
              list.add(entry.getValue());
            }
          }
        }
      }
    } else {
      if (redisUtils.hasKey(key)) {
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        if (toMap) {
          list.add(entries);
        } else {
          for (Map.Entry<Object, Object> entry : entries.entrySet()) {
            list.add(entry.getValue());
          }
        }
      }
    }
    // redis 为空, 查询一级缓存 caffeine
    if (openCaffeineCache && list.isEmpty()) {
      ConcurrentMap<String, Object> soc = caffeineCache.asMap();
      for (Map.Entry<String, Object> soe : soc.entrySet()) {
        String soeKey = soe.getKey();
        if (blurKey != null) {
          if (soeKey.contains(key) && soeKey.contains(blurKey)) {
            Object value = soe.getValue();
            if (value instanceof HashMap) {
              HashMap<Object, Object> map = (HashMap<Object, Object>) value;
              if (toMap) {
                list.add(map);
              } else {
                for (Map.Entry<Object, Object> entry : map.entrySet()) {
                  list.add(entry.getValue());
                }
              }
            } else {
              list.add(soe.getValue());
            }
          }
        } else {
          if (soeKey.contains(key)) {
            Object value = soe.getValue();
            if (value instanceof HashMap) {
              HashMap<Object, Object> map = (HashMap<Object, Object>) value;
              if (toMap) {
                list.add(map);
              } else {
                for (Map.Entry<Object, Object> entry : map.entrySet()) {
                  list.add(entry.getValue());
                }
              }
            } else {
              list.add(soe.getValue());
            }
          }
        }
      }
    }
    return list;
  }

  /**
   * 追加或替换 zset 数据
   *
   * @param key 键值
   * @param value 数据
   * @param source 下标
   * @param time 过期时间
   * @param timeUnit 过期单位
   */
  @SuppressWarnings("unchecked")
  public void zsetAdd(String key, Object value, long source, long time, TimeUnit timeUnit) {
    /*    String caffeineKey = getCaffeineZsetKey(key);
    CaffeineZsetMap czm = new CaffeineZsetMap();
    List<CaffeineZsetMap> caffeineZsetMaps = (List<CaffeineZsetMap>) caffeine.get(caffeineKey);
    if (caffeineZsetMaps == null) {
      caffeineZsetMaps = new ArrayList<>();
      caffeineZsetMaps.add(czm.buildOne(source, value));
    } else {
      int last = caffeineZsetMaps.size();
      caffeineZsetMaps.add(czm.build(last, source, value));
    }
    caffeine.putIfNoneRefresh(caffeineKey, caffeineZsetMaps);
    caffeineSetKeyExpired(caffeineKey, time, timeUnit);*/
    redisUtils.zsetCoverByResource(key, value, source);
    redisTemplate.expire(key, time, timeUnit);
  }

  /**
   * 获取 hash 数据
   *
   * @param key 哈希目录键值
   * @param hashKey 键值
   * @param <T> 泛型
   * @return data
   */
  @SuppressWarnings("unchecked")
  public <T> T getByHash(String key, String hashKey) {
    Object data = null;
    boolean hasHashKey = redisUtils.hasHashKey(key, hashKey);
    if (hasHashKey) {
      data = redisTemplate.opsForHash().get(key, hashKey);
    } else if (openCaffeineCache) {
      if (caffeineKeyIsOut(key)) {
        caffeineCache.invalidate(key);
        return null;
      }
      data = caffeine.getHash(key, hashKey);
    }

    return (T) data;
  }

  /**
   * 获取 zset 列表中下标数据
   *
   * @param key 键值
   * @param source 下标
   * @param <T> 泛型
   * @return 数据
   */
  @SuppressWarnings("unchecked")
  public <T> T getZsetMember(String key, long source) {
    Object data = null;
    String caffeineKey = getCaffeineZsetKey(key);
    boolean out = caffeineKeyIsOut(caffeineKey);
    if (out) {
      caffeineCache.invalidate(caffeineKey);
      Set<Object> range = redisTemplate.opsForZSet().range(key, source, source);
      if (range != null && !range.isEmpty()) {
        Optional<Object> first = range.stream().findFirst();
        data = first.get();
      }
    } else {
      List<CaffeineZsetMap> caffeineZsetMaps = (List<CaffeineZsetMap>) caffeine.get(caffeineKey);
      for (CaffeineZsetMap caffeineZsetMap : caffeineZsetMaps) {
        if (caffeineZsetMap.getSource() == source) {
          data = caffeineZsetMap;
          break;
        }
      }
    }
    return (T) data;
  }

  /**
   * 获取 zset 的列表
   *
   * @param key 键值
   * @param <T> 泛型
   * @return 泛型列表
   */
  @SuppressWarnings("unchecked")
  public <T> List<T> getZsetList(String key) {
    List<T> list = new ArrayList<>();
    String caffeineKey = getCaffeineZsetKey(key);
    boolean out = caffeineKeyIsOut(caffeineKey);
    if (out) {
      caffeineCache.invalidate(caffeineKey);
      Set<Object> range = redisTemplate.opsForZSet().range(key, 0, -1);
      if (range != null && !range.isEmpty()) {
        list = range.stream().map(data -> (T) data).collect(Collectors.toList());
      }
    } else {
      List<CaffeineZsetMap> caffeineZsetMaps = (List<CaffeineZsetMap>) caffeine.get(caffeineKey);
      list = caffeineZsetMaps.stream().map(map -> (T) map.getMember()).collect(Collectors.toList());
    }
    return list;
  }

  /**
   * 根据 key 值更换数据 obj
   *
   * @param key 键值
   * @param newObj 新的数据
   */
  public void replaceValue(String key, Object newObj) {
    Object obj = get(key);
    if (obj != null) {
      if (newObj != null) {
        redisTemplate.opsForValue().set(key, newObj);
        if (openCaffeineCache) {
          caffeine.putIfNoneRefresh(key, newObj);
        }
        return;
      }
      throw new IllegalArgumentException(key + " is not found");
    }
  }

  public void replaceKey(String oldKey, String newKey) {
    if (StringUtils.hasText(newKey)) {
      Object obj = get(oldKey);
      if (obj != null) {
        ExpiredTime expired = getExpired(oldKey);
        // 继承过期时间
        redisTemplate
            .opsForValue()
            .set(newKey, obj, Duration.of(expired.getRedisTime(), ChronoUnit.SECONDS));
        if (openCaffeineCache) {
          caffeine.putIfNoneRefresh(newKey, obj);
          caffeine.putIfNoneRefresh(EXPIRED_PREFIX_KEY + newKey, expired.getCaffeineTime());
        }
        return;
      }
      throw new IllegalArgumentException(oldKey + " obj is not exits");
    }
  }

  public void replaceKeyAndValue(String oldKey, String newKey, Object newObj) {
    if (StringUtils.hasText(newKey)) {
      if (hasKey(oldKey)) {
        ExpiredTime expired = getExpired(oldKey);
        redisTemplate
            .opsForValue()
            .set(newKey, newObj, Duration.of(expired.getRedisTime(), ChronoUnit.SECONDS));
        if (openCaffeineCache) {
          caffeine.putIfNoneRefresh(newKey, newObj);
          caffeine.putIfNoneRefresh(EXPIRED_PREFIX_KEY + newKey, expired.getCaffeineTime());
        }
        remove(oldKey);
        return;
      }
      throw new IllegalArgumentException(oldKey + " is not exits");
    }
  }

  /**
   * 获取缓存剩余时间
   *
   * <p>因为 redis 存在有效期时，缓存流程中 caffeine 也存在。但是如果 redis 存在但是 caffeine 不存在，则视为 caffeine 过期
   *
   * @param key 键值
   * @return 时间对象
   */
  public ExpiredTime getExpired(String key) {
    long keyExpire = redisUtils.getKeyExpire(key);
    Object obj = caffeine.get(EXPIRED_PREFIX_KEY + key);
    if (obj != null) {
      return new ExpiredTime(keyExpire, (Long) obj);
    }
    return new ExpiredTime(keyExpire, -1L);
  }
  /**
   * 获取数据
   *
   * <p>先从 redis 中获取数据，因为存在过期执行需求，当 redis 过期时可以从存活时间比它长的一级缓存 caffeine 中获取数据。如果 caffeine
   * 缓存在指定时间内没被调用它也会过期。
   *
   * @param key 键值
   * @param <T> 泛型
   * @return 数据
   */
  @SuppressWarnings("unchecked")
  public <T> T get(String key) {
    Object data = null;
    boolean hasRedis = redisUtils.hasKey(key);
    if (hasRedis) {
      data = redisTemplate.opsForValue().get(key);
    } else if (openCaffeineCache) {
        if (caffeineKeyIsOut(key)) {
          caffeineCache.invalidate(key);
          return null;
        }
      data = caffeine.get(key);
    }
    return (T) data;
  }

  /**
   * 判断键值是否存在
   *
   * <p>实践上当 redis 存在有效期时，{@code caffeine} 也理应存在有效期，并且它的有效期大于或等于 redis。因此{@link #EXPIRED_PREFIX_KEY}
   * 是必然存在的。
   *
   * <p>那么当 redis 失效时，它也理应根据 {@link #EXPIRED_PREFIX_KEY} 中的 {@code value}和{@link #differenceTime}
   * 值之和来对应与当前{@code 系统时间 / 1000}作比较，系统时间较大，则意味着过期。同时删除对应的键值。
   *
   * <p>综上所述，如果存在 redis 有过期效应的同时，caffeine 没有设置 {@link #EXPIRED_PREFIX_KEY}，那么将当前 key 视为空。并返回 false
   *
   * @param key 键值
   * @return 是否存在
   */
  public boolean hasKey(String key) {
    boolean hasRedis = redisUtils.hasKey(key);
    if (hasRedis) {
      return true;
    } else if (openCaffeineCache) {
      Object obj = caffeine.get(EXPIRED_PREFIX_KEY + key);
      if (obj != null) {
        // 存在过期时间
        long timestamp = (long) obj;
        boolean has = System.currentTimeMillis() / 1000 < (timestamp + differenceTime);
        if (!has) {
          // 删除一级缓存
          caffeine.remove(key);
          caffeine.remove(EXPIRED_PREFIX_KEY + key);
        }
        return has;
      }
    }
    return false;
  }

  /**
   * 删除简单缓存
   *
   * @param key 缓存键值
   */
  public void remove(String key) {
    caffeine.remove(key);
    if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
      redisTemplate.delete(key);
    }
  }

  /**
   * 前缀模糊删除
   *
   * <p>删除相同的前缀值
   *
   * @param prefix 前缀
   */
  public void blurPrefixRemove(String prefix) {
    Set<String> keys = redisUtils.blurHasKey(prefix + "*");
    for (String key : keys) {
      remove(key);
    }
  }

  public <T> T get(String key, Class<T> clazz) {
    return get(key);
  }

  /**
   * 获取 caffeine 的 zset 队列
   *
   * <p>mode == 0 时根据 id 排序，mode == 1 时则根据 source 排序
   *
   * @param mode 排序
   * @return caffeine zset 队列
   */
  @SuppressWarnings("unchecked")
  private List<CaffeineZsetMap> getCaffeineZsetList(String key, Integer mode) {
    List<CaffeineZsetMap> caffeineZsetMaps =
        (List<CaffeineZsetMap>) caffeine.get(CAFFEINE_ZSET_KEY + key);
    if (!caffeineZsetMaps.isEmpty()) {
      if (mode == 0) {
        caffeineZsetMaps.sort((m1, m2) -> Long.compare(m2.getId(), m1.getId()));
      } else if (mode == 1) {
        caffeineZsetMaps.sort((m1, m2) -> Double.compare(m2.getSource(), m1.getSource()));
      }
    }
    return caffeineZsetMaps;
  }

  /**
   * 设置 caffeine 缓存的过期时间
   *
   * <p>该流程中 caffeine 必须是含有当前 key 的缓存。
   *
   * <p>caffeine 的有效时间是根据 {@link #differenceTime} 和 {@link TimeUtils#getTime(Integer, TimeUnit)}
   * 的时间差进行。当 differenceTime >= 0 时确保了一级缓存大于等于二级缓存的有效时间
   *
   * @param key 键值
   * @param time 时间
   * @param timeUnit 时间单位 {@link TimeUtils#getTime(Integer, TimeUnit)} 注意方法中涵盖的单位
   */
  private void caffeineSetKeyExpired(String key, long time, TimeUnit timeUnit) {
    if (caffeine.hasKey(key)) {
      long timestamp = TimeUtils.getTime((int) time, timeUnit);
      caffeineCache.put(
          EXPIRED_PREFIX_KEY + key, System.currentTimeMillis() / 1000 + timestamp + differenceTime);
    } else {
      throw new RuntimeException("键值不存在");
    }
  }

  /**
   * 检查缓存是否过期
   *
   * <p>如果键值不存在也视为过期
   *
   * @param key 键值
   * @return 是否过期
   */
  private boolean caffeineKeyIsOut(String key) {
    if (caffeine.hasKey(key)) {
      long time = (long) caffeine.get(EXPIRED_PREFIX_KEY + key);
      return (time + this.differenceTime) < System.currentTimeMillis() / 1000;
    }
    return true;
  }

  /**
   * 获取 caffeine 在对应二级缓存中 opsForHash 的键值
   *
   * @param hashKey 哈希键值
   * @param key 键值
   * @return 对应键值
   */
  private String getCaffeineHashKey(String hashKey, String key) {
    return hashKey + "::" + key;
  }
  /**
   * 获取 caffeine 在对应二级缓存中 zset 的键值
   *
   * @param key 键值
   * @return 对应键值
   */
  private String getCaffeineZsetKey(String key) {
    return CAFFEINE_ZSET_KEY + key;
  }

  @Data
  @AllArgsConstructor
  static class ExpiredTime {
    long redisTime;
    long caffeineTime;
  }
}
